using System;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using ePipe;
using eTask.Extensions;
using Letter.IO.Memorizer;
using Letter.Net.Sockets;

namespace Letter.Net.Session;

sealed partial class UdpSession<TSnders>
{
    private static readonly int BufferSize = MemoryPoolProvider.GetBlockSize();
    
    sealed class UdpSessionSnder
    {
        public UdpSessionSnder(UdpSession<TSnders> transport)
        {
            this.transport = transport;
        }

        private UdpSession<TSnders> transport;
        private Task sndTask;

        private UdpSnder currentSender;
        
        private IPipeReader<byte> Output
        {
            get { return transport.Application.Reader; }
        }

        private IPipeWriter<byte> Input
        {
            get { return transport.Application.Writer; }
        }

        private ASocketSnderProvider<UdpSnder> Snders
        {
            get { return this.transport.SocketSnders; }
        }

        private Socket Socket
        {
            get { return this.transport.socket; }
        }
        
        public void Start()
        {
            this.sndTask = this.StartSndAsync();
        }
        
         private async Task StartSndAsync()
        {
            var output = this.Output;
            var socket = this.Socket;
            var snders = this.Snders;

            Exception error = null;

            try
            {
                while (true)
                {
                    var result = await output.ReadAsync();
                    if (result.IsCanceled || result.IsCompleted)
                    {
                        break;
                    }
                    
                    var buffer = result.Buffer;
                    if (!buffer.IsEmpty)
                    {
                        if (buffer.Length > BufferSize)
                        {
                            throw new Exception($"The size of the udp data sent at A time cannot exceed {BufferSize}");
                        }

                        this.currentSender = snders.Rent();

                        var remAddress = output.Token as EndPoint;
                        if (this.transport.RemoteEndPoint != null)
                        {
                            if (remAddress != null)
                            {
                                throw new Exception("In connection mode, only fixed point-to-point transmission is supported");
                            }

                            remAddress = this.transport.RemoteEndPoint;
                        }
                        else
                        {
                            if (remAddress == null)
                            {
                                throw new Exception("No remote destination address is provided");
                            }
                        }

                        var transferResult = await this.currentSender.SendAsync(socket, remAddress, buffer);
                        if (!SocketOperationUtil.TryNormalCompletion(transferResult, out error))
                        {
                            break;
                        }

                        snders.Return(this.currentSender);
                        this.currentSender = null;
                    }
                    output.AdvanceTo(buffer.End);
                    output.Flush();
                }
            }
            catch (ObjectDisposedException ex)
            {
                error = ex;
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                if (!this.transport.closed)
                {
                    if (error != null)
                    {
                        this.transport.Connection.OnException(error);
                    }

                    this.transport.CloseAsync().Ignore();
                }
            }
        }
        
        public async ValueTask CloseAsync()
        {
            this.AbortSocketSnderIO();

            await sndTask;
        }

        private void AbortSocketSnderIO(Exception error= null)
        {
            this.Output.Complete(error);
            this.Input.CancelPendingFlush();

            if (this.currentSender != null)
            {
                try
                {
                    this.currentSender.Dispose();
                    this.currentSender = null;
                }
                catch
                {
                }
            }
        }
    }
}
